#!/usr/bin/env python
import abc
import json
import os
import shutil
import sys

from PIL import Image

_BLACK = (0, 0, 0)

def _image_to_array(path):
    global _BLACK
    
    image = Image.open(path)
    string = image.tostring()
    (width, height) = image.size
    pixel_width = 4
    del image
    
    array = []
    for i in range(height):
        row = i * width * pixel_width
        slice = []
        for j in range(width):
            index = row + (j * pixel_width)
            pixel = string[index:index + pixel_width]
            visible = pixel[3] != '\x00'
            if not visible:
                colour = None
            else:
                colour = tuple((ord(c) for c in pixel[:3]))
                if colour == _BLACK:
                    colour = None
            slice.append((colour, visible))
        array.append(slice)
    return array

class _Area(object):
    _colour = None
    _left = None
    _right = None
    _top = None
    _bottom = None
    
    def __init__(self, colour, x, y):
        self._colour = colour
        self._left = x
        self._top = y
        
    def set_bottom_right(self, x, y):
        self._right = x
        self._bottom = y

    def _validate(self):
        if self._left is None or self._right is None or self._top is None or self._bottom is None:
            raise Exception("Incomplete shape definition : " + str(self._colour))
        if (self._right - self._left) % 2 or (self._bottom - self._top) % 2:
            raise Exception("Shapes must be multiples of 2 in width and height")
            
    def serialise(self):
        self._validate()
        
        width = self._right - self._left
        height = self._bottom - self._top
        return {
         'width': width,
         'height': height,
         'centre': (
          int(self._left + (width / 2)),
          int(self._top + (height / 2)),
         ),
         'circular': width == height,
        }

class _Orientable(_Area):
    __meta__ = abc.ABCMeta
    _orientation_marker = None
    _orientation = None
    
    def add_orientation_marker(self, x, y):
        left = x == self._left + 1
        top = y == self._top + 1
        
        if self._orientation_marker:
            if self._orientation_marker[0]:
                if top:
                    self._orientation = 'up'
                elif self._orientation_marker[1] and left:
                    self._orientation = 'left'
                elif not self._orientation_marker[1] and not left:
                    self._orientation = 'right'
            elif not top:
                self._orientation = 'down'

            if not self._orientation:
                raise Exception("Unable to determine orientation : " + str(self._colour))
        else:
            self._orientation_marker = (top, left)

    def _validate(self):
        _Area._validate(self)

        if self._orientation_marker and not self._orientation:
            raise Exception("Incomplete orientation : " + str(self._colour))

    def serialise(self):
        serialisation = _Area.serialise(self)
        serialisation['orientation'] = self._orientation
        return serialisation
        
class _Engine(_Orientable):
    def _validate(self):
        _Orientable._validate(self)
        
        if self._orientation is None:
            raise Exception("Engines must be oriented : " + str(self._colour))
            
class _Weapon(_Orientable):
    _WEAPON_SIZE = 16
    
    def _validate(self):
        _Orientable._validate(self)
        
        width = self._right - self._left
        height = self._bottom - self._top
        if width % self._WEAPON_SIZE or height % self._WEAPON_SIZE:
            raise Exception("Invalid size (%(width)i, %(height)i): %(colour)s" % {
             'width': width,
             'height': height,
             'colour': str(self._colour),
            })
            
def _process(array, object, orientable):
    objects = {}
    ordered_objects = []
    for (y, line) in enumerate(array):
        obj = None
        for (x, (colour, visible)) in enumerate(line):
            if visible:
                if not obj:
                    if not colour and orientable:
                        raise Exception("Started weapon-block with orientation-marker")
                    obj = objects.get(colour)
                    if not obj:
                        obj = objects[colour] = object(colour, x, y)
                        ordered_objects.append(obj)
                elif not colour and orientable:
                    obj.add_orientation_marker(x, y)
            else:
                if obj: #The current area's done
                    obj.set_bottom_right(x, y + 1)
                    obj = None
        if obj: #Close it, since it hit the right edge
            obj.set_bottom_right(x + 1, y + 1)
    return [obj.serialise() for obj in ordered_objects]
    
def _process_dimensions(path):
    image = Image.open(path + 'shell.png')
    (width, height) = image.size
    return {
     'width': width,
     'height': height,
    }
    image.close()
    
def _process_weapons(path):
    return _process(_image_to_array(path + 'weapons.png'), _Weapon, orientable=True)

def _process_engines(path):
    return _process(_image_to_array(path + 'engines.png'), _Engine, orientable=True)

def _process_hitboxes(path):
    return {
     'hull': _process(_image_to_array(path + 'hull.png'), _Area, orientable=False),
     'shields': _process(_image_to_array(path + 'shields.png'), _Area, orientable=False),
     'power': _process(_image_to_array(path + 'power.png'), _Area, orientable=False),
     'sensors': _process(_image_to_array(path + 'sensors.png'), _Area, orientable=False),
    }
    
def _process_paint(path):
    return {
     'base': _process(_image_to_array(path + 'paint_base.png'), _Area, orientable=False),
     'top': _process(_image_to_array(path + 'paint_top.png'), _Area, orientable=False),
    }

def _is_positive_integer(value):
    return type(value) is int and value >= 0

def _is_positive_number(value):
    return type(value) in (int, float) and value >= 0
    
def _validate_engine_config(specs):
    if specs['effect'] not in (
     'ribbon', #draw a quadrilateral with start-edge where the ship is now, extend it to the distance travelled at 75% size, set opacity high, and then shrink the dimensions and increase transparency with each frame until it disappears
     'flare', #a simple combustion effect with two emitters: one creates fire and the other creates rays
     'glow', #creates a texture of size appropriate for the engine and magnifies its area and opacity as speed increases
     'strobe', #similar to glow, except the radius doesn't increase. Instead, a copy of the texture fades out from wherever the ship was each frame
    ):
        raise ValueError("Invalid engine-effect specified: " + specs['effect'])

    colour = [float(c) for c in specs['colour'] if type(c) in (int, float) and 0.0 <= c <= 1.0]
    if not len(colour) == 3:
        raise ValueError("Engine-colour not specified as a triple of intensities between 0 and 1")
    specs['colour'] = colour #Sanitise it

    if not _is_positive_number(specs['thrust']):
        raise ValueError("Thrust must be a positive number, expressed as a value in Newtons")
        #thrust is affected by power availability, engine boosters, and computational boosters
        #Thrust is an additive force, 'cause of, y'know, vaccuum

    if not _is_positive_number(specs['thrustPowerDraw']):
       raise ValueError("Thrust power-draw must be a positive number, expressed as a value in megawatts") 
       #The power-draw required to run the engines at full output
       #For simplicity's sake, draw will probably be linear
       #deceleration will be effected by "dropping anchor" and applying full engine force in the opposite direction,
       #reducing velocity linearly, in addition to any linear damping caused by gravitational fields

    if not _is_positive_number(specs['maxSpeed']):
        raise ValueError("Maximum speed must be a positive number, expressed as a value in m/s")
        #max-speed is intended to be the upper limit at which the ship's deflector-shields can keep up with movement
        #shielding is affected by power availability, shielding boosters, and computational boosters

    if not _is_positive_number(specs['maxSpeedPowerDraw']):
        raise ValueError("Maximum speed power-draw must be a positive number, expressed as a value in megawatts")
        #The number of megawatts needed to sustain deflector shields at top speed
        #Draw is inverse-square (so it takes a lot of power to travel at high speeds, offset by the
        #engines having to work less to add velocity)
        #This can be statically computed outside of combat and disasters, also allowing max thrust to be
        #a static value

    if not _is_positive_number(specs['tacking']):
        raise ValueError("Tacking must be a positive number, expressed in degrees/s")
        #this is how fast the ship can rotate each second, based on lateral thrusters
        #the ship's current velocity is not relevant to this value, nor is it additive, because it
        #would take the same amount of force to slow down anyway
        #This is affected by power availability and engine boosters (not computational)

    if not _is_positive_number(specs['tackingPowerDraw']):
        raise ValueError("Tacking power-draw must be a positive number, expressed in megawatts")
        #Tacking always has priority over all non-weapon systems, and these values should normally
        #be pretty small, so this is just useful for making a crippled ship actually behave crippled

def _validate_chassis_config(specs):
    if not _is_positive_number(specs['mass']):
        raise ValueError("Mass must be a positive number, expressed as a value in tonnes")

    if not _is_positive_integer(specs['minCrew']):
        raise ValueError("Minimum crew must be a positive number, expressed as a value in headcount")

    if not _is_positive_integer(specs['baseCrew']):
        raise ValueError("Base crew must be a positive number, expressed as a value in headcount")

    if not _is_positive_integer(specs['volume']):
        raise ValueError("Volume must be a positive number, expressed as a value in units of space, excluding minimum crew")

    if not _is_positive_integer(specs['modSlots']):
        raise ValueError("Modification slots must be a positive number")
        #Every mod consumes a certain amount of volume and adds to mass
        #They all expose a common "modifier" interface that takes ship stats and returns transformed ship stats,
        #allowing for internal static computation

    if not _is_positive_number(specs['powerOutput']):
        raise ValueError("Power output must be a positive number, expressed as a value in megawatts")
        #This is the ship's available power per cycle when in peak condition
        #Damage will linearly degrade this value

def _validate_shields_config(specs):
    if not _is_positive_integer(specs['capacity']):
        raise ValueError("Shield capacity must be a positive number, expressed as a value in damage units")
        #The amount of damage shields can absorb
        
    if not _is_positive_number(specs['draw']):
        raise ValueError("Shield draw must be a positive number, expressed as a value in megawatts")
        #The amount of energy needed to maintain shielding
        #Shields are only active during combat, and engine draw is usually negligable at that time

    if not _is_positive_number(specs['drawRecovery']):
        raise ValueError("Shield draw recovery must be a positive number, expressed as a value in megawatts")
        #The amount of energy needed to restore shields by 1% strength

    if not _is_positive_number(specs['recoverySpeed']):
        raise ValueError("Shield recovery speed must be a positive number, expressed as a value in seconds")
        #How many seconds it takes for shields to recover by 1%

    if not _is_positive_number(specs['reinitialisationTime']):
        raise ValueError("Shield reinitialisation time must be a positive number, expressed as a value in seconds")
        #Once shields have failed, they cannot be restored for this many seconds
        #Base sheild power-draw must be available throughout this period and it will supersede all other needs;
        #if insufficient power is available, for that frame, the timer will not decrease, but the
        #power will be available for other needs

    if not _is_positive_number(specs['reinitialisationThreshold']):
        raise ValueError("Shield reinitialisation threshold must be a positive number, expressed as a value in percent")
        #The percentage charge required before shields will be reinitialised

def _validate_integrity_config(specs):
    #Subsystems and hull damage cannot be repaired into the next 25% bracket out of port
    #Crew will repair the hull with 50% of available resources, shields at 20%, and the others at 10%

    if not _is_positive_integer(specs['hull']):
        raise ValueError("Hull integrity must be a positive number, expressed as a value in damage units")
        #At 0%, the ship is destroyed. Other values don't have any effect.
        
    if not _is_positive_integer(specs['sensors']):
        raise ValueError("Sensor integrity must be a positive number, expressed as a value in damage units")
        #As sensors take damage, the ship's weapons behave poorly and computational multipliers fall
        #Below 75%, weapons will shoot where the enemy currently is, with no lead
        #Below 50%, targets may not be acquired
        #Below 25%, weapons use severly restricted angular correction values
        #At 0%, weapons are disabled
        #Outside of combat, 75% = no distant ships on mini-map (best ship's sensors will be used)
        #50% = no ships
        #25% = no ports
        #0% = no mini-map

    if not _is_positive_number(specs['sensorsRepairRate']):
        raise ValueError("Sensor repair-rate must be a positive number, expressed as a value in damage units")
        #The amount of damage every otherwise-unoccupied crew-member can repair each second

    if not _is_positive_integer(specs['shields']):
        raise ValueError("Shield-generator integrity must be a positive number, expressed as a value in damage units")
        #As generators take damage, the effectiveness of power-transferrence falls
        #Below 75%, the amount of power needed to regenerate shielding increases by 50%
        #Below 50%, shields take 50% more power to maintain and maximum capacity is reduced by 25%
        #Below 25%, shields take 50% longer to reinitialise and maximum capacity is reduced by 50%
        #At 0%, shields are inoperable
        #Outside of combat, maximum speed is multiplied by 50% of current shield strength + 50%

    if not _is_positive_number(specs['shieldsRepairRate']):
        raise ValueError("Shield repair-rate must be a positive number, expressed as a value in damage units")

    if not _is_positive_integer(specs['power']):
        raise ValueError("Power integrity must be a positive number, expressed as a value in damage units")
        #As generators take damage, the amount of generated power falls linearly
        #Behvaiour is the same outside of combat

    if not _is_positive_number(specs['powerRepairRate']):
        raise ValueError("Power repair-rate must be a positive number, expressed as a value in damage units")

    if not _is_positive_integer(specs['engines']):
        raise ValueError("Engine integrity must be a positive number, expressed as a value in damage units")
        #As engines take damage, their effectiveness falls linearly
        #Behvaiour is the same outside of combat

    if not _is_positive_number(specs['enginesRepairRate']):
        raise ValueError("Engine repair-rate must be a positive number, expressed as a value in damage units")

def _validate_generic_config(specs):
    if not _is_positive_integer(specs['baseCost']):
        raise ValueError("Base cost must be a positive integer, expressed in SPAAAAAAACE-credits")
        
def _process_specs(path):
    specs = json.loads(open(path + 'specs.json').read())
    _validate_engine_config(specs['engine'])
    _validate_chassis_config(specs['chassis'])
    _validate_shields_config(specs['shields'])
    _validate_integrity_config(specs['integrity'])
    _validate_generic_config(specs['generic'])
    return specs
    
def process_ship(source_path, dest_path):
    json_spec = {
     'dimensions': _process_dimensions(source_path),
     'hitboxes': _process_hitboxes(source_path),
     'weapons': _process_weapons(source_path),
     'engines': _process_engines(source_path),
     'paint': _process_paint(source_path),
     'specs': _process_specs(source_path),
    }
    if not os.path.isdir(dest_path):
        os.mkdir(dest_path)
    shutil.copy(source_path + "base.png", dest_path)
    shutil.copy(source_path + "shell.png", dest_path)
    open(dest_path + "ship.json", 'w').write(json.dumps(json_spec, separators=(',', ':')))
    
if __name__ == '__main__':
    base_path = os.path.abspath('.') + os.path.sep
    if not os.path.isdir(base_path + 'ships_dist'):
        os.mkdir(base_path + 'ships_dist')

    source_path = base_path + 'ships' + os.path.sep
    dest_path = base_path + 'ships_dist' + os.path.sep

    ships = None
    if len(sys.argv) > 1:
        ships = sys.argv[1:]
    else:
        ships = [f for f in os.listdir(source_path) if os.path.isdir(source_path + f)]
    for ship in ships:
        print "Processing " + ship + "..."
        process_ship(source_path + ship + os.path.sep, dest_path + ship + os.path.sep)
        
